Phase 2: Manual Device Entry#2
Merged
Merged
Conversation
- Add tests/test_config_flow.py with 8 test functions: test_manual_add_menu_shown, test_manual_add_creates_subentry, test_manual_add_mode_flag, test_manual_add_invalid_enum, test_manual_add_duplicate_enum, test_manual_add_device_name, test_manual_add_position_step_timed_only, test_reconfigure_timed_motor_aborts - Add 3 cover tests to tests/test_cover.py: test_cover_mode_attribute, test_cover_mode_defaults_bidirectional_when_key_absent, test_cover_initial_position_from_subentry - All tests RED: fail on missing CONF_BIDIRECTIONAL/CONF_INITIAL_POSITION symbols and missing flow methods (expected RED state)
…, cover reads
- const.py: add CONF_BIDIRECTIONAL ('bidirectional') and CONF_INITIAL_POSITION
- config_flow.py: async_step_blind now delegates to async_step_menu; add
async_step_menu (shows 'Pair automatically' / 'Add manually' menu),
async_step_manual_add (validates 2-hex enum, dedup check, mode, name),
async_step_manual_position (initial position slider, timed only); extend
__init__ with _pending_is_bidirectional/_pending_initial_position; add
timed-motor guard at top of async_step_reconfigure (REVIEW-2)
- cover.py: add CONF_BIDIRECTIONAL/CONF_INITIAL_POSITION imports; read both
in __init__ (CONF_BIDIRECTIONAL default True for legacy subentries, REVIEW-1);
add extra_state_attributes returning mode; seed initial position in
async_added_to_hass (RestoreEntity takes precedence)
- strings.json + translations/en.json: relabel initiate_flow.user to 'Add device'
(REVIEW-3); add menu/manual_add/manual_position steps, invalid_enum_format +
duplicate_enum errors, timed_calibration_unavailable abort
- tests/test_config_flow.py: fix handler binding (_make_handler helper with tuple
handler + context source=user); all 11 Phase-2 tests now GREEN (34 pass)
…g, full gate
- tests/test_config_flow.py: add test_manual_add_enum_case_normalized (lowercase
'1a' collides with existing '1A' after .upper()), test_manual_add_enum_stored_uppercase
('2b' stored as '2B')
- tests/test_cover.py: add test_cover_initial_position_clamped (CONF_INITIAL_POSITION=150
clamps to 100; restored prior state of 50 beats seeded initial of 100)
- Full WSL pytest suite: 160 passed, 0 failures (no regressions)
- Native ruff: All checks passed; mypy: Success, no issues in 3 source files
… (CR-01)
In _async_position_update_loop, the position_reached branch now clears
_attr_is_opening, _attr_is_closing, _attr_is_closed, and _target_position
before returning -- matching the two boundary-exit branches that already
did this correctly. Without this fix, timed motors were left stuck in
a phantom opening/closing state indefinitely after SET_POSITION completed.
Also adds _unrecorded_attributes = frozenset({'mode'}) to SchellenbergCover
so the static mode attribute is excluded from the recorder (WR-05).
…ion, and cleanups (WR-01/03/04, IN-01/02) - WR-01: async_step_manual_position now aborts with pairing_failed if _pending_device_enum is missing, preventing a broken empty-id subentry. - WR-03: BooleanSelector default and resolver default for CONF_BIDIRECTIONAL flipped from False to True so an unmodified form yields bidirectional, matching the field-common case and the read-default used everywhere else. - WR-04: removed the never-used _pending_initial_position attribute from __init__. - IN-01: Awaitable moved from typing to collections.abc (deprecated in 3.9+). - IN-02: Unicode en-dash in comment replaced with plain ASCII hyphen.
… orphaned menu key (WR-02, IN-04) - WR-02: manual_position step description now notes that positioning for timed motors uses a default travel time and is uncalibrated until Phase 4. - IN-04: removed the orphaned config.step.menu.menu_options.pair_device_menu key from both strings.json and translations/en.json; no matching flow step exists. Subentry-level user/manual_add keys are unaffected.
…and WR-03 default (WR-06) - test_timed_motor_position_loop_clears_flags: exercises the REAL _async_position_update_loop for a timed motor with tiny travel time, drives SET_POSITION to 50, and asserts is_opening=False, is_closing=False, _target_position=None, and position==50 after completion (WR-06/CR-01). - test_manual_position_aborts_without_pending_state: asserts abort with pairing_failed when manual_position is entered without prior manual_add (WR-01). - test_manual_add_default_mode_is_bidirectional: asserts that omitting CONF_BIDIRECTIONAL from the form yields CONF_BIDIRECTIONAL=True (WR-03).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QwPMgtiypLgJ5nkR3mUGfL
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Phase 2: Manual Device Entry
Goal: Users can add an already-paired Schellenberg motor to Home Assistant by entering its known 2-char hex transmit address, selecting bidirectional or timed mode, and giving it a name — no auto-pairing, no calibration.
Status: Verified ✓ (automated)
Adds a "manual add" branch to the blind subentry flow: a method menu (auto-pair vs manual), a validated hex-address form with timed/bidirectional mode selection, a conditional initial-position step for timed motors, the chosen mode persisted in
subentry.data, and the mode surfaced as a covermodeattribute. This also lands thebidirectionalflag that Phase 3 (timed motor control) depends on.Changes
Manual-add slice (
config_flow.py,const.py,cover.py,strings.json,translations/en.json)async_step_menu→async_step_manual_add→ (timed)async_step_manual_positionflow;async_step_blindnow opens the menu, auto-pair (async_step_user) stays a branch.CONF_BIDIRECTIONAL/CONF_INITIAL_POSITIONconstants; mode stored as a real bool insubentry.data.re.match(r"^[0-9A-Fa-f]{2}$")), case-normalized duplicate detection, friendly-name fallback.SchellenbergCoverreads the mode (missing-key default True = legacy-safe bidirectional), exposesextra_state_attributes = {"mode": ...}(excluded from recorder via_unrecorded_attributes), and seeds the initial position inside the RestoreEntity fallback (RestoreEntity wins).async_step_reconfigureaborts timed motors withtimed_calibration_unavailableinstead of hanging in the event-waiting calibration handler.Code-review hardening
is_opening/is_closing/is_closed/_target_position— previously a timed motor stayed "moving" forever after aset_positionand poisoned the next move.device_idsubentry; removed dead state and an orphaned translation key;Awaitablefromcollections.abc.Tests (
tests/test_config_flow.py,tests/test_cover.py)Requirements Addressed
SETUP-01, SETUP-02, SETUP-03, SETUP-04, SETUP-05, SETUP-06.
Verification
modeattribute).Key Decisions
CONF_BIDIRECTIONALreads asTrue(legacy auto-paired motors are bidirectional) — prevents a Phase-3 control regression; distinct from the manual-add form default.device_id(entry-only scope) — accepted, documented limitation; inbound 6-char frame matching is a future story.